Vue SpringBoot实现Html和Markdown格式内容(含图片上传)保存到MySQL

实现功能

  1. 本文代码实现了前端Markdown格式的博文保存到MySQL的功能。
  2. 包括文章中图片的上传,在用户选择图片后就将其传到后端并将图片的链接返回给前端,填入到指定的位置。

遇到的问题

  1. 由于Markdown编辑器原因,返回的图片路径不能有\与空格

  2. 如果遇到第二次进入编辑页面不能显示文章内容,那么在下方getArticle()方法中,处理响应的最后一行加入

    1
    2
    // 解决第二次进入不能显示内容bug
    this.$refs.md.d_value = response.data.markdownContent
  3. Html格式内容中的部分特殊符号会被JAVA替换掉,导致回显的页面样式有出入。建议不使用Html格式,使用Markdown格式

前端

安装依赖

  1. 数据请求相关
    1
    2
    npm install axios --save
    npm install qs --save
  1. 安装Markdown编辑器

    1
    npm install mavon-editor --save

    使用mavon-editor,请自行参考如何使用。目前不支持流程图、序列图、甘特图

  2. 其他依赖

    1
    2
    3
    4
    npm install style-loader
    npm install css-loader
    npm install sass-loader
    npm install babel-loader --save

在main.js中引入mavonEditor

1
2
3
import mavonEditor from 'mavon-editor'
import 'mavon-editor/dist/css/index.css'
Vue.use(mavonEditor)

ArticleMarkdown.vue组件,实现Markdown博文的存取

代码导读

getArticle():通过文章id获取内容
saveArticle():提交博文内容到后端。同时提交了html、markdown格式的内容,见

1
2
var htmlCode = this.$refs.md.d_render; 
var markdownCode = this.$refs.md.d_value;

imgAdd(pos, file):上传单张图片。file图片对象,pos图片下标,后端返回图片链接地址时,用于定位
imgDel(pos):删除图片
mulUploadimg() :上传多张图片。图片对象存放在data中img_file对象中
imgDelMul(pos):删除多张图片。

源代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
<template>
<div>
<mavon-editor ref="md" class="md" v-model="sqlData.markdown" @imgAdd="imgAdd" @imgDel="imgDel" @save="saveArticle"/>
</div>
</template>

<script>
import axios from 'axios'
import qs from 'qs'
const area_axios = axios.create({
headers: {'Content-Type': 'application/json;charset=utf-8',},// 设置传输内容的类型和编码
withCredentials: true,// 指定某个请求应该发送凭据
})
const file_axios = axios.create({
headers: {'Content-Type': 'multipart/form-data',},// 设置传输内容的类型和编码
withCredentials: true,// 指定某个请求应该发送凭据
})
const area_form_axios = axios.create({
headers: {'Content-Type': 'application/x-www-form-urlencoded',},// 设置传输内容的类型和编码
withCredentials: true,// 指定某个请求应该发送凭据
})
export default {
name: "Markdown",
data() {
return {
sqlData:{
markdown:'',
html:''
},
img_file: {},// 一次上次多张图片时使用
};
},
mounted:function (){
getArticle()
},
methods: {
// 获取文章
getArticle(){
area_form_axios.get('/api/get',{
params:{id: 12 }
},)
.then(response => {
console.log(this.sqlData)
this.sqlData = response.data
})
.catch(err => {
alert("请求失败")
})
},
// 保存文章
saveArticle(){
var htmlCode = this.$refs.md.d_render;
var markdownCode = this.$refs.md.d_value;
if(htmlCode.length == 0 || markdownCode.length == 0){
alert("请填写")
return;
}
area_axios({
url: '/api/add',
method: 'post',
data: JSON.stringify({'markdown':markdownCode,'html':htmlCode}),
}).then((response) => {
if(response.data > 0){
alert("成功")
}else {
alert("失败")
}
})
},
// 添加图片
imgAdd(pos, file){
console.log("pos:"+pos)
// 第一步.将图片上传到服务器.
var formdata = new FormData();
formdata.append('pic', file);
file_axios({
url: '/api/img_upload',
method: 'post',
data: formdata,
}).then((response) => {
// 第二步.将返回的url替换到文本原位置
var url = response.data;
//通过引入对象获取: import {mavonEditor} from ... 等方式引入后,此时$vm即为mavonEditor
//通过$refs获取: html声明ref : <mavon-editor ref=md ></mavon-editor>, 此时$vm为 this.$refs.md`
this.$refs.md.$img2Url(pos, url);
})
},
// 删除图片
imgDel(pos){
console.log("imgDel pos:"+pos)
},
// 多张图片
mulUploadimg(){
// 第一步.将图片上传到服务器.
var formdata = new FormData();
for(var _img in this.img_file){
debugger
// 后台需要图片的key一致
formdata.append('pics', this.img_file[_img]);
}
file_axios({
url: '/api/mul_img_upload',
method: 'post',
data: formdata,
}).then((res) => {
/**
* 例如:返回数据为 res = [[pos, url], [pos, url]...]
* pos 为原图片标志(0)
* url 为上传后图片的url地址
*/
// 第二步.将返回的url替换到文本原位置![...](0) -> ![...](url)
var idx_url = res.data;
idx_url.forEach(item => {
//通过引入对象获取: import {mavonEditor} from ... 等方式引入后,此时$vm即为mavonEditor
//通过$refs获取: html声明ref : <mavon-editor ref=md ></mavon-editor>, 此时$vm为 this.$refs.md`
this.$refs.md.$img2Url(item[0], item[1]);
});
})
},
// 多张图片
imgDelMul(pos){
console.log("imgDel pos:"+pos)
delete this.img_file[pos];
},
}
}
</script>

跨域配置

vue-axios 前后端分离 跨域访问的实现

后端

文件上传相关配置

application.properties文件中

1
2
3
4
5
spring.servlet.multipart.enabled=true
# 最大支持文件大小
spring.servlet.multipart.max-file-size=10MB
# 最大支持请求大小
spring.servlet.multipart.max-request-size=50MB

配置拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component
public class CrossDomainInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 允许客户端携带跨域cookie,此时origin值不能为“*”,只能为指定单一域名。!!开发时不要使用localhost访问
response.setHeader("Access-Control-Allow-Credentials", "true");
// 允许指定域访问跨域资源
//response.setHeader("Access-Control-Allow-Origin", "http://127.0.0.1:9006, http://127.0.0.1:8080");
response.setHeader("Access-Control-Allow-Origin", origin);// *
// 允许浏览器发送的请求消息头
//response.setHeader("Access-Control-Allow-Headers", "*");
response.setHeader("Access-Control-Allow-Headers", request.getHeader("Access-Control-Request-Headers"));
// 允许浏览器在预检请求成功之后发送的实际请求方法名
//response.setHeader("Access-Control-Allow-Methods", "DEFAULT,POST,PATCH,PUT,OPTIONS,DELETE,HEAD");
response.setHeader("Access-Control-Allow-Methods", request.getHeader("Access-Control-Request-Method"));
// 浏览器缓存预检请求结果时间,单位:秒
response.setHeader("Access-Control-Max-Age", "86400");
return true;
}
}

数据接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Controller
@RequestMapping("")
public class MarkdownController {
/**
* 获取文章
* id: 文章id
* @author YSL
* 2019-03-04 15:38
*/
@GetMapping("/get")
@ResponseBody
public Bean test(@RequestParam("id")Integer id){
// 获取数据库中的数据,请自行实现。
return vueMarkdownMapper.query(id);
}

/**
* 保存文章到数据库。
* bean:前端传回JSON.stringify({'markdown':markdownCode,'html':htmlCode})格式的数据即可
* @author YSL
* 2019-03-04 15:26
*/
@PostMapping("/add")
@ResponseBody
public int test(@RequestBody Bean bean){
return vueMarkdownMapper.add(bean);// 保存数据到数据库,请自行实现
}
}

图片上传

代码导读

@RequestMapping(“/img_upload”):单张图片上传,上传到blog_files/pictures目录下,返回图片url。
@RequestMapping(“/mul_img_upload”):多张图片上传。上传到blog_files/pictures目录下,返回new String[]{图片下标, 图片url}格式的list。
fileUpload():文件上传。上传到blog_files/files目录下。
upload():真正实现文件上传的方法,基于MultipartFile实现

说明

  1. 图片与文件都是上传到tomcat/webapps/blog_files/目录下,blog_files是我专门用来保存图片的一个web工程,方便通过http访问到图片,给前端返回的图片地址也是http格式的。
  2. 在图片和文件上传的同时会备份,案例总备份路径:D:/webserver_bak/blog/
源代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
public class FileController {
/**
* 图片上传(一张)
* @param pic 需要上传的图片
* @return 图片url
* @author YSL
* 2019-03-01 17:14
*/
@RequestMapping("/img_upload")
@ResponseBody
public String imgUpload(@RequestParam(value = "pic", required = false) MultipartFile pic, HttpServletRequest request){

List<String> urlList = upload(new MultipartFile[]{pic}, "pictures", request);

return urlList != null ? urlList.get(0) : "";
}

/**
* 图片上传(多张)
* @param pics 需要上传的图片
* @return 图片下标和url
* @author YSL
* 2019-03-01 17:14
*/
@RequestMapping("/mul_img_upload")
@ResponseBody
public List<String[]> imgUpload(@RequestParam(value = "pics", required = false) MultipartFile[] pics, HttpServletRequest request){

List<String> urlList = upload(pics, "pictures", request);

List<String[]> list = new ArrayList<>();
for (int i = 0; i < urlList.size() ; i++) {
String[] idx_url = new String[2];

// 图片下标
idx_url[0]=i+"";
// 拼接url
idx_url[1] = urlList.get(i);

list.add(idx_url);
}

return list;
}

/**
* 文件上传
* @param files 需要上传的文件
* @return 文件url
* @author YSL
* 2019-03-01 17:14
*/
@RequestMapping("/file_upload")
@ResponseBody
public List<String> fileUpload(@RequestParam(value = "files", required = false) MultipartFile[] files, HttpServletRequest request){
List<String> urlList = upload(files, "pictures", request);
return urlList;
}

/**
* 文件/图片上传。并做备份<br/>
* 路径不能有反斜线和空格 <br/>
* 上传路径:.../webapps/blog_files/pictures/20190301/图片 <br/>
* 上传路径:.../webapps/blog_files/files/20190301/文件 <br/>
* 备份路径:.../webserver_bak/blog/pictures/20190301/图片 <br/>
* 备份路径:.../webserver_bak/blog/files/20190301/文件
* @param files 需要上传的文件
* @param categoryPath 类别路径,pictures/files
* @return 上传成功,返回文件url
* @author YSL
* 2019-03-01 16:45
*/
public List<String> upload(MultipartFile[] files, String categoryPath, HttpServletRequest request){

// 非空判定
if(files == null || files.length == 0){
return new ArrayList<>();
}

// 专门存放文件工程名称(是一个javaweb工程,方便图片直接通过http访问)
String fileProject = "blog_files";
// 备份路径
String bakPath = "D:/webserver_bak/blog/";blog_files
//http://localhost:7989/
String ipPort = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort() + "/";

/**
* 获取项目绝对路径,格式,D:\tomcats\apache-tomcat-8.0.52\webapps\boot\。
* markdown编辑器图片路径不能有\,所以替换为/
* 注意:.replace("//", "/"); 与 replaceAll("\\\\", "/");
*/
String rootPath = request.getSession().getServletContext().getRealPath("").replaceAll("\\\\", "/");

// 项目路径。/boot
String contextPath = request.getContextPath();
rootPath = rootPath.substring(0, rootPath.lastIndexOf(contextPath.replace("/","")));

StringBuilder fileRoot = new StringBuilder("");
// 工程名称
fileRoot.append(fileProject);
fileRoot.append("/");
// 类别目录
fileRoot.append(categoryPath);
fileRoot.append("/");
// 文件目录,图片上传失败时使用
String picRootPath = fileRoot.toString();
String day = new SimpleDateFormat("yyyyMMdd").format(new Date());
// 日期目录
fileRoot.append(day);
fileRoot.append("/");

// 文件最终保存目录
String fileDir = fileRoot.toString();

List<String> list = new ArrayList<>();
for (MultipartFile multipartFile : files) {

// 文件名称。markdown编辑器图片路径不能有空格
String upFileName = multipartFile.getOriginalFilename().replaceAll("\\s+", "");
String filename = new SimpleDateFormat("HHmmss").format(new Date()) + "_" + UUID.randomUUID().toString() + "_" + upFileName;

String filePathName = rootPath + fileDir + filename;
File destFile = new File(filePathName);
try {
// 复制临时文件到指定目录下, 会创建没有的目录
FileUtils.copyInputStreamToFile(multipartFile.getInputStream(), destFile);

// 拼接url
list.add(ipPort + fileDir + filename);

// 备份
File bakFile = new File(bakPath + fileDir + filename);
FileUtils.copyInputStreamToFile(multipartFile.getInputStream(), bakFile);
} catch (UnsupportedEncodingException e2) {
e2.printStackTrace();
if("pictures".equals(categoryPath)){
// 默认图片
list.add(picRootPath+"default.jpg");
}else{
list.add("");
}
} catch (IOException e) {
e.printStackTrace();
if("pictures".equals(categoryPath)){
// 默认图片
list.add(picRootPath+"default.jpg");
}else{
list.add("");
}
}
}

return list;
}
}

数据库

字段名类型长度备注
idint默认文章id
markdowntextmarkdown格式内容
htmltexthtml格式内容

参考
https://blog.csdn.net/qq_32407233/article/details/84656914
https://blog.csdn.net/wangjun5159/article/details/48809427
https://segmentfault.com/q/1010000016563395

------------- 本文结束  感谢您的阅读 -------------
评论